Skip to content

fix: ensure FlutterResult and invokeMethod are always called on main thread#98

Open
muzahidul-opti wants to merge 5 commits intomasterfrom
fix/ios16-main-thread-channel
Open

fix: ensure FlutterResult and invokeMethod are always called on main thread#98
muzahidul-opti wants to merge 5 commits intomasterfrom
fix/ios16-main-thread-channel

Conversation

@muzahidul-opti
Copy link
Contributor

@muzahidul-opti muzahidul-opti commented Mar 6, 2026

Summary

Fixes a threading violation where FlutterResult / MethodChannel.Result was being called from a background thread on both iOS and Android, causing Dart Futures to hang or crash with a TypeError.

iOS fix (SwiftOptimizelyFlutterSdkPlugin.swift)

Added a mainThreadResult() private helper that wraps every FlutterResult callback to ensure it is always dispatched on the main thread via DispatchQueue.main.async. Applied once in handle() so all 25+ method handlers (current and future) are automatically protected.

Android fix (OptimizelyFlutterSdkPlugin.java)

Added a safeResult() private helper that wraps MethodChannel.Result and dispatches success, error, and notImplemented to the main thread via Handler(Looper.getMainLooper()). Applied once in onMethodCall() so all 28 handlers (current and future) are automatically protected.

Dart fix — wrapper (optimizely_client_wrapper.dart)

Added _invoke() static helper replacing all 18 raw Map<String, dynamic>.from(await _channel.invokeMethod(...)) call sites. Handles null returns and PlatformException gracefully, preventing TypeError crashes.

Dart fix — user context (optimizely_user_context.dart)

Added identical _invoke() instance helper replacing all 14 raw invokeMethod call sites.

Unit tests (test/invoke_safety_test.dart)

17 new tests covering the two previously-uncovered error branches in _invoke() — null return and PlatformException — for both OptimizelyClientWrapper and OptimizelyUserContext. Overall unit test coverage: 85.9% → 87.3%.

Root Causes

  1. OptimizelyClient.start() (iOS) and async SDK callbacks (Android) fire on background threads → FlutterResult/Result called from wrong thread → response may be dropped or cause undefined behaviour → Dart Future hangs forever.
  2. When the response is dropped, native returns nullMap<String, dynamic>.from(null)TypeError crash in Dart.

Platform Comparison

iOS Android
Helper mainThreadResult(_ result:) -> FlutterResult safeResult(Result result) -> Result
Main-thread check Thread.isMainThread Looper.myLooper() == Looper.getMainLooper()
Dispatch DispatchQueue.main.async { result(value) } new Handler(Looper.getMainLooper()).post(...)
Applied in handle() onMethodCall()

Test plan

  • Run unit tests: flutter test — all 140 tests pass, coverage 87.3%
  • Run flutter analyze — no new warnings
  • Run integration tests on an iOS 16 simulator: flutter test integration_test/tests/ios16_threading_regression.dart -d <ios-16-simulator-id>
  • All integration tests should pass: top-level SDK methods, user context methods, concurrent initializeClient stress test

🤖 Generated with Claude Code

muzahidul-opti and others added 4 commits March 6, 2026 16:41
…thread

iOS 16 requires FlutterResult to be invoked on the main thread. When
OptimizelyClient.start() or decideAsync closures call result() from a
background thread under multi-SDK startup contention, iOS 16 silently
drops the response causing the Dart Future to never resolve or return
null, which previously crashed the app with an unhandled TypeError or
PlatformException.

Two layers fixed:

1. iOS native (SwiftOptimizelyFlutterSdkPlugin.swift)
   - Added mainThreadResult() private helper that wraps FlutterResult
     to always dispatch on the main thread (no-op if already on main)
   - Applied once in handle() so every current and future method handler
     is protected automatically — no per-handler changes needed

2. Dart layer (OptimizelyClientWrapper + OptimizelyUserContext)
   - Added _invoke() helper that wraps every MethodChannel.invokeMethod
     call with a null guard and PlatformException catch
   - Null response returns {success:false} instead of crashing via
     Map.from(null) → TypeError
   - PlatformException is caught and returned as {success:false}
   - All 18 call sites in OptimizelyClientWrapper and 14 in
     OptimizelyUserContext now route through _invoke

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ore entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…branches

Covers the two previously-uncovered error branches in the iOS 16 fix:
- Native returns null → success:false (no TypeError)
- Native throws PlatformException → success:false (no unhandled exception)

Tests both OptimizelyClientWrapper and OptimizelyUserContext.
Overall unit test coverage: 85.9% → 87.3%

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…roid)

Mirrors the iOS mainThreadResult() fix. Added safeResult() wrapper in
onMethodCall() that dispatches success/error/notImplemented to the Android
main thread via Handler(Looper.getMainLooper()) when called from a
background thread. Applied once so all current and future handlers are
automatically protected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@muzahidul-opti muzahidul-opti changed the title fix: ensure FlutterResult and invokeMethod are always called on main thread (iOS 16) fix: ensure FlutterResult and invokeMethod are always called on main thread Mar 6, 2026
Flutter 3.0.5 (used in CI) exposes instance as nullable — use ?.
pattern consistent with existing test suite to fix compilation failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants